flyteplugins-agents-crewai 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,18 @@
1
+ """CrewAI adapter for Flyte.
2
+
3
+ Bring your own CrewAI ``Agent`` and run it durably on Flyte. The adapter
4
+ provides:
5
+
6
+ - :func:`tool` — turn a Flyte ``@env.task`` into a CrewAI tool that executes as
7
+ a durable child action (own container/GPU, retries, caching).
8
+ - :func:`run_agent` — run the CrewAI agent loop inside your task and return the
9
+ final answer.
10
+
11
+ Each tool call runs as a durable Flyte child action, and the run timeline is
12
+ rendered into the Flyte task report.
13
+ """
14
+
15
+ from ._run import run_agent, run_agent_sync
16
+ from ._tools import tool
17
+
18
+ __all__ = ["run_agent", "run_agent_sync", "tool"]
@@ -0,0 +1,166 @@
1
+ """Durable, replayable model turns for CrewAI.
2
+
3
+ CrewAI owns the agent loop and drives the model itself: during
4
+ ``Agent.kickoff_async`` it calls its ``crewai.LLM`` once per turn. To make that
5
+ loop durable we swap in a durable ``LLM``: it records each turn through the
6
+ shared :func:`~flyteplugins.agents.core.durable_step` (a ``flyte.trace`` leaf),
7
+ so inside a Flyte task a crashed/retried run replays completed turns from their
8
+ recorded completions instead of re-calling (and re-billing) the model. Tool
9
+ calls run as durable child actions (see :func:`flyteplugins.agents.crewai.tool`),
10
+ so the whole run becomes crash-resilient when the enclosing task carries
11
+ ``retries=...``.
12
+
13
+ Two CrewAI facts shape the implementation:
14
+
15
+ - ``crewai.LLM(model=...)`` is a *factory*: ``LLM.__new__`` dispatches on the
16
+ model name and returns a provider-specific subclass (``OpenAICompletion``,
17
+ ``AnthropicCompletion``, ...). A plain ``class D(LLM)`` therefore never sees the
18
+ provider's real ``call``. We instead subclass the *concrete* provider class of a
19
+ probe instance (``type(LLM(model=...))``) and instantiate through it, so the
20
+ durable overrides sit directly on top of the real completion methods.
21
+ - ``kickoff_async`` invokes the **synchronous** ``call`` (verified on 1.15.2),
22
+ not ``acall``; we override both so durability applies whichever path runs.
23
+
24
+ The turn result is the completion — a string, or an arbitrary object for
25
+ structured outputs. Strings round-trip as-is; anything else is coerced to a
26
+ string for the trace record (structured turns are recorded but rebuilt as text,
27
+ which is a lossy but safe fallback and never the case for the default text loop).
28
+ The concrete provider class is a heavy import, so the durable subclass is built
29
+ lazily on first use.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import typing
35
+
36
+ from flyteplugins.agents.core import durable_step, fingerprint, jsonable
37
+
38
+ # Sentinel prefix marking a completion we serialized as JSON (non-string result).
39
+ # Plain string completions are stored verbatim; on load we only JSON-decode a
40
+ # value carrying this prefix, so ordinary strings never get mis-parsed.
41
+ _JSON_PREFIX = "\x00json\x00"
42
+
43
+
44
+ def _messages_fingerprint(
45
+ messages: typing.Any,
46
+ tools: typing.Any,
47
+ extra: typing.Mapping[str, typing.Any] | None = None,
48
+ ) -> str:
49
+ """Deterministic memo key for a model turn — message content + tool names.
50
+
51
+ Fingerprints on serializable request identity only: the messages (a str or a
52
+ list of ``{"role", "content"}`` dicts) and the tool *names* — never callables
53
+ or live tool/agent objects.
54
+ """
55
+ payload: dict[str, typing.Any] = {
56
+ "messages": jsonable(messages),
57
+ "tools": sorted(_tool_names(tools)),
58
+ }
59
+ if extra:
60
+ payload.update(extra)
61
+ return fingerprint(payload)
62
+
63
+
64
+ def _tool_names(tools: typing.Any) -> list[str]:
65
+ """Extract stable tool names from CrewAI's ``[{name: BaseTool}, ...]`` (or list)."""
66
+ names: list[str] = []
67
+ for entry in tools or []:
68
+ if isinstance(entry, dict):
69
+ names.extend(str(k) for k in entry.keys())
70
+ else:
71
+ names.append(str(getattr(entry, "name", entry)))
72
+ return names
73
+
74
+
75
+ def _dumps(result: typing.Any) -> str:
76
+ """Serialize a turn result: strings verbatim, everything else JSON-tagged.
77
+
78
+ JSON-native structures (dict/list/scalars) serialize directly; SDK objects
79
+ are coerced via ``jsonable`` first. ``default=str`` catches any residue so
80
+ serialization never fails a turn.
81
+ """
82
+ if isinstance(result, str):
83
+ return result
84
+ import json
85
+
86
+ if isinstance(result, (dict, list, int, float, bool)) or result is None:
87
+ payload = result
88
+ else:
89
+ payload = jsonable(result)
90
+ return _JSON_PREFIX + json.dumps(payload, default=str)
91
+
92
+
93
+ def _loads(recorded: str) -> typing.Any:
94
+ """Rebuild a turn result from its recorded string form."""
95
+ if recorded.startswith(_JSON_PREFIX):
96
+ import json
97
+
98
+ return json.loads(recorded[len(_JSON_PREFIX) :])
99
+ return recorded
100
+
101
+
102
+ def _make_durable_llm_class(model: str) -> type:
103
+ """Build a durable ``LLM`` subclass over the concrete provider for ``model``.
104
+
105
+ Imported/derived lazily: ``crewai`` and its provider classes are heavy, and
106
+ the concrete class depends on the model name (provider dispatch).
107
+ """
108
+ from crewai import LLM
109
+
110
+ probe = LLM(model=model)
111
+ concrete_cls = type(probe)
112
+
113
+ class DurableLLM(concrete_cls): # type: ignore[valid-type, misc]
114
+ """A ``crewai.LLM`` whose every model turn is recorded via ``durable_step``.
115
+
116
+ ``call`` (used by ``kickoff_async``) and ``acall`` both route the real
117
+ completion through the shared durable step so retries replay recorded
118
+ turns. Durability is guarded: if the trace layer misbehaves the real
119
+ call still runs, so it never breaks a run.
120
+ """
121
+
122
+ def call(self, messages: typing.Any, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
123
+ from flyte._utils.asyn import run_sync
124
+
125
+ tools = args[0] if args else kwargs.get("tools")
126
+ try:
127
+ return run_sync(
128
+ durable_step,
129
+ _messages_fingerprint(messages, tools),
130
+ lambda: _as_awaitable(super(DurableLLM, self).call(messages, *args, **kwargs)),
131
+ name="model_turn",
132
+ dumps=_dumps,
133
+ loads=_loads,
134
+ )
135
+ except Exception: # pragma: no cover - durability never breaks a run
136
+ return super().call(messages, *args, **kwargs)
137
+
138
+ async def acall(self, messages: typing.Any, *args: typing.Any, **kwargs: typing.Any) -> typing.Any:
139
+ tools = args[0] if args else kwargs.get("tools")
140
+ try:
141
+ return await durable_step(
142
+ _messages_fingerprint(messages, tools),
143
+ lambda: _as_awaitable(super(DurableLLM, self).acall(messages, *args, **kwargs)),
144
+ name="model_turn",
145
+ dumps=_dumps,
146
+ loads=_loads,
147
+ )
148
+ except Exception: # pragma: no cover - durability never breaks a run
149
+ return await _as_awaitable(super().acall(messages, *args, **kwargs))
150
+
151
+ return DurableLLM
152
+
153
+
154
+ async def _as_awaitable(value: typing.Any) -> typing.Any:
155
+ """Normalize a maybe-coroutine to an awaited value (``call`` returns eagerly)."""
156
+ import inspect
157
+
158
+ if inspect.isawaitable(value):
159
+ return await value
160
+ return value
161
+
162
+
163
+ def make_durable_llm(model: str) -> typing.Any:
164
+ """Construct a durable ``crewai.LLM`` instance for the given ``model`` name."""
165
+ cls = _make_durable_llm_class(model)
166
+ return cls(model=model)
@@ -0,0 +1,70 @@
1
+ """Cross-run CrewAI memory — a thin handle over Flyte's keyed ``MemoryStore``.
2
+
3
+ CrewAI keeps conversation state in-memory and ``kickoff_async`` does not thread a
4
+ prior transcript back out (its ``LiteAgentOutput.messages`` holds only the
5
+ system+user turns of the current call, not the assistant reply). So this module
6
+ maintains the transcript itself: it resolves a keyed ``MemoryStore`` and stores
7
+ the running conversation — a list of ``{"role", "content"}`` dicts — in a
8
+ path-addressed JSON slot. A later run with the same ``memory_key`` loads that
9
+ transcript and prepends it to the new prompt, continuing the conversation.
10
+
11
+ Everything here is best-effort: a store that can't be resolved, or a read/write
12
+ that fails, degrades to a memoryless run rather than breaking it.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import typing
18
+
19
+ from flyte._logging import logger
20
+ from flyteplugins.agents.core import resolve_memory as _resolve_memory
21
+
22
+ # Path-addressed memory slot holding a thread's conversation transcript
23
+ # (a list of ``{"role", "content"}`` message dicts) inside the MemoryStore.
24
+ _HISTORY_PATH = "crewai/history.json"
25
+
26
+
27
+ async def resolve_memory(memory_key: str | None) -> typing.Any | None:
28
+ """Resolve a keyed MemoryStore for CrewAI cross-run memory, or ``None``.
29
+
30
+ Best-effort: returns ``None`` when ``memory_key`` is falsy or no durable
31
+ store can be resolved, so memory never breaks a run.
32
+ """
33
+ if not memory_key:
34
+ return None
35
+ return await _resolve_memory(memory_key)
36
+
37
+
38
+ async def load_history(store: typing.Any) -> list[dict[str, typing.Any]]:
39
+ """Load the stored conversation transcript, or ``[]`` if none/unavailable."""
40
+ if store is None:
41
+ return []
42
+ try:
43
+ history = await store.read_json.aio(_HISTORY_PATH, default=[])
44
+ except Exception: # pragma: no cover - memory is best-effort, never fatal
45
+ logger.warning("Could not load CrewAI conversation history; continuing without it.")
46
+ return []
47
+ return history if isinstance(history, list) else []
48
+
49
+
50
+ def build_input(history: list[dict[str, typing.Any]], user_input: str) -> list[dict[str, typing.Any]]:
51
+ """Build the kickoff message list from prior transcript + the new user turn."""
52
+ return [*history, {"role": "user", "content": user_input}]
53
+
54
+
55
+ async def save_turn(store: typing.Any, user_input: str, assistant_output: str) -> None:
56
+ """Append the user + assistant turns to the transcript and persist it.
57
+
58
+ Best-effort: any failure is logged and swallowed so a run never breaks on
59
+ memory persistence.
60
+ """
61
+ if store is None:
62
+ return
63
+ try:
64
+ history = await load_history(store)
65
+ history.append({"role": "user", "content": user_input})
66
+ history.append({"role": "assistant", "content": assistant_output})
67
+ await store.write_json.aio(_HISTORY_PATH, history)
68
+ await store.save.aio()
69
+ except Exception: # pragma: no cover - memory is best-effort, never fatal
70
+ logger.warning("Could not persist CrewAI conversation history; continuing.")
@@ -0,0 +1,158 @@
1
+ """``run_agent`` — run a CrewAI agent on Flyte.
2
+
3
+ CrewAI owns the agent loop (it drives the model + tools). ``run_agent`` runs that
4
+ loop inside your ``@env.task``: it builds an agent with Flyte-task tools, drives
5
+ the agent via ``Agent.kickoff_async``, and returns the final answer. Each tool
6
+ call runs as a durable Flyte child action (its own container/resources, with
7
+ retries and caching).
8
+
9
+ Observability: the run timeline — tool calls and AI message turns — is rendered
10
+ into the Flyte task report.
11
+
12
+ The adapter minimizes delta between native CrewAI code and Flyte integration by
13
+ exposing tools that are drop-in ``crewai.tools.BaseTool`` instances.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import typing
19
+
20
+ from flyteplugins.agents.core import ReportTimeline, flush_report, sync_variant
21
+
22
+ from . import _memory
23
+ from ._durable import make_durable_llm
24
+ from ._tools import _coerce_tool
25
+
26
+ if typing.TYPE_CHECKING:
27
+ from crewai import Agent as _CrewAgent
28
+ else:
29
+ _CrewAgent = None
30
+
31
+
32
+ def _extract_text(result: typing.Any) -> str:
33
+ """Pull the final answer out of a CrewAI ``LiteAgentOutput`` (or fallback)."""
34
+ if result is None:
35
+ return ""
36
+ raw = getattr(result, "raw", None)
37
+ if isinstance(raw, str):
38
+ return raw
39
+ return str(result)
40
+
41
+
42
+ async def run_agent(
43
+ input: str,
44
+ *,
45
+ tools: typing.Sequence[typing.Any] = (),
46
+ model: str | None = None,
47
+ instructions: str | None = None,
48
+ agent: typing.Any = None,
49
+ name: str = "crewai-agent",
50
+ durable: bool = True,
51
+ observability: bool = True,
52
+ memory_key: str | None = None,
53
+ **run_kwargs: typing.Any,
54
+ ) -> str:
55
+ """Run a CrewAI agent with the given tools and prompt; return the final text.
56
+
57
+ Await this from an async task as ``await run_agent(...)``; from a sync task
58
+ use :func:`run_agent_sync` instead.
59
+
60
+ Call this from inside an ``@env.task`` — that task is the durable parent.
61
+ Within it, each tool call runs as a durable Flyte child action. Give the
62
+ enclosing task ``retries=...`` for self-healing and ``report=True`` to see
63
+ the agent timeline.
64
+
65
+ Provide either a pre-built ``agent`` (with its own tools already attached) or
66
+ ``tools`` + ``model`` to have one built for you — not both.
67
+
68
+ Args:
69
+ input: The user prompt.
70
+ tools: ``tool``-wrapped tools or bare ``@env.task`` templates. Attached
71
+ natively to the built ``Agent(tools=...)``. Ignored when ``agent`` is
72
+ given (a pre-built agent carries its own tools).
73
+ model: Model name (e.g. ``"gpt-4o"``) for the built agent. Required on
74
+ the builder path (no default is assumed — the adapter is provider
75
+ agnostic); ignored when a pre-built ``agent`` is given.
76
+ instructions: Extra guidance folded into the built agent's backstory.
77
+ agent: A pre-built CrewAI ``Agent``. Mutually exclusive with ``tools``.
78
+ name: Agent name (for debugging/observability).
79
+ durable: Record/replay each model turn via ``flyte.trace``. Applied only
80
+ when ``run_agent`` builds the agent (the builder sets a durable
81
+ ``llm``); a pre-built ``agent`` keeps its own ``llm`` and is not
82
+ rewrapped, so its turns are not durable.
83
+ observability: Render the run timeline into the Flyte task report.
84
+ memory_key: Stable id (e.g. a user/thread id) for cross-run memory.
85
+ When set, conversation history is persisted to a keyed ``MemoryStore``
86
+ and resumed on a later run with the same key.
87
+ **run_kwargs: Additional kwargs forwarded to ``Agent.kickoff_async``.
88
+
89
+ Returns:
90
+ The agent's final output as a string.
91
+ """
92
+ timeline = ReportTimeline() if observability else None
93
+ if timeline is not None:
94
+ timeline.heading("CrewAI agent")
95
+
96
+ if agent is not None and tools:
97
+ raise ValueError("Pass either `agent` (with its own tools) or `tools`, not both.")
98
+
99
+ # Build the agent if not provided, attaching the Flyte-task tools natively.
100
+ if agent is None:
101
+ if model is None:
102
+ raise ValueError(
103
+ 'No model was given. Provide `model=` (e.g. "gpt-4o") when building the agent '
104
+ "(or pass a pre-built `agent=`)."
105
+ )
106
+ if _CrewAgent is None:
107
+ from crewai import Agent as _LocalAgent
108
+ else:
109
+ _LocalAgent = _CrewAgent
110
+
111
+ backstory = f"You are a helpful assistant named {name}."
112
+ if instructions:
113
+ backstory = f"{backstory}\n\n{instructions}"
114
+
115
+ # When durable, drive the agent with a durable ``LLM`` so every model
116
+ # turn is recorded via ``flyte.trace`` and replayed on retry. For a
117
+ # PREBUILT agent we cannot safely rewrap its ``llm``, so durability is
118
+ # only applied on this builder path (see the module docstring / limitation).
119
+ if durable:
120
+ llm: typing.Any = make_durable_llm(model)
121
+ else:
122
+ llm = model
123
+
124
+ agent = _LocalAgent(
125
+ role="Assistant",
126
+ goal="Answer the user's question accurately and concisely.",
127
+ backstory=backstory,
128
+ tools=[_coerce_tool(t) for t in tools],
129
+ llm=llm,
130
+ )
131
+
132
+ # Cross-run memory: load the prior transcript (if any) and prepend it to the
133
+ # new user turn, so ``kickoff_async`` continues the conversation. Best-effort.
134
+ store = await _memory.resolve_memory(memory_key)
135
+ if store is not None:
136
+ history = await _memory.load_history(store)
137
+ kickoff_input: typing.Any = _memory.build_input(history, input)
138
+ else:
139
+ kickoff_input = input
140
+
141
+ # Drive the agent loop. ``kickoff_async`` takes the prompt (a str, or a list
142
+ # of ``{"role", "content"}`` message dicts when resuming memory) as its input;
143
+ # a pre-built agent already carries its own tools, so none are injected here.
144
+ result = await agent.kickoff_async(kickoff_input, **run_kwargs)
145
+
146
+ final = _extract_text(result)
147
+
148
+ # Persist this turn to the transcript for the next run with the same key.
149
+ if store is not None:
150
+ await _memory.save_turn(store, input, final)
151
+
152
+ if observability:
153
+ await flush_report()
154
+
155
+ return final or ""
156
+
157
+
158
+ run_agent_sync = sync_variant(run_agent)
@@ -0,0 +1,229 @@
1
+ """Turn Flyte tasks into CrewAI tools that execute as durable actions.
2
+
3
+ CrewAI requires tools attached to an ``Agent(tools=[...])`` to be
4
+ ``crewai.tools.BaseTool`` instances — plain callables are rejected by pydantic
5
+ validation. :func:`tool` therefore wraps a Flyte ``@env.task`` as a ``BaseTool``
6
+ subclass whose execution dispatches to the task via ``task.aio()`` — so when the
7
+ agent calls the tool, it runs as a durable Flyte child action (its own
8
+ container/resources, with retries and caching) rather than inline in the agent's
9
+ process.
10
+
11
+ Sync/async bridge: CrewAI invokes tools synchronously (``BaseTool.run`` ->
12
+ ``_run``; the agent loop routes through ``CrewStructuredTool.invoke``, which calls
13
+ ``self.func`` — our ``_run`` — and, if it returns a coroutine, ``asyncio.run``s it).
14
+ ``asyncio.run`` explodes inside the already-running loop of a Flyte task, so we make
15
+ ``_run`` a *synchronous* method that bridges to ``task.aio()`` via
16
+ :func:`flyte._utils.asyn.run_sync`, which drives the coroutine on a dedicated
17
+ background-thread loop and works from within a running loop. ``_arun`` awaits the
18
+ task directly for CrewAI's native async path.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import functools
24
+ import inspect
25
+ import json
26
+ import typing
27
+ from functools import partial
28
+
29
+ from flyte._task import AsyncFunctionTaskTemplate
30
+ from flyte._utils.asyn import run_sync
31
+ from flyte.models import NativeInterface
32
+ from flyteplugins.agents.core import attach_tool_resolver, coerce_tool_args, task_json_schema
33
+
34
+
35
+ def tool(
36
+ func: AsyncFunctionTaskTemplate | typing.Callable | None = None,
37
+ *,
38
+ name: str | None = None,
39
+ description: str | None = None,
40
+ ) -> typing.Any:
41
+ """Convert a Flyte task (or plain callable) into a CrewAI ``BaseTool``.
42
+
43
+ - For an ``@env.task``: returns a ``BaseTool`` whose execution runs the task as
44
+ a durable Flyte child action when the agent invokes it. The input schema is
45
+ derived from the task via the Flyte type engine. The backing task is wired
46
+ to :class:`~flyteplugins.agents.core.ToolTaskResolver` and exposed via
47
+ ``__wrapped_task__`` so it resolves to itself on the worker (no recursion).
48
+ - For a plain (async) callable: returns a ``BaseTool`` that runs it inline.
49
+
50
+ The returned object is a native ``crewai.tools.BaseTool`` instance, so it can be
51
+ attached directly to ``Agent(tools=[...])``.
52
+
53
+ Usable bare, parametrized, or as a direct call::
54
+
55
+ @tool
56
+ @env.task
57
+ async def get_weather(city: str) -> str: ...
58
+ """
59
+ if func is None:
60
+ return partial(tool, name=name, description=description)
61
+ if isinstance(func, AsyncFunctionTaskTemplate):
62
+ return _task_to_tool(func, name=name, description=description)
63
+ return _callable_to_tool(func, name=name, description=description)
64
+
65
+
66
+ def _args_model_from_signature(fn: typing.Callable, model_name: str) -> type:
67
+ """Build a pydantic model describing ``fn``'s parameters for CrewAI's args schema.
68
+
69
+ CrewAI derives a tool's args schema from ``BaseTool.args_schema`` (or, failing
70
+ that, from the ``_run`` signature). Our ``_run`` takes ``**kwargs``, so we hand
71
+ CrewAI an explicit model built from the wrapped callable's annotations.
72
+ """
73
+ from pydantic import create_model
74
+
75
+ try:
76
+ hints = typing.get_type_hints(fn)
77
+ except Exception: # pragma: no cover - unresolved annotations
78
+ hints = {}
79
+ fields: dict[str, typing.Any] = {}
80
+ for pname, param in inspect.signature(fn).parameters.items():
81
+ if pname in ("self", "return"):
82
+ continue
83
+ if param.kind in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD):
84
+ continue
85
+ fallback = param.annotation if param.annotation is not inspect.Parameter.empty else typing.Any
86
+ annotation = hints.get(pname, fallback)
87
+ if param.default is inspect.Parameter.empty:
88
+ fields[pname] = (annotation, ...)
89
+ else:
90
+ fields[pname] = (annotation, param.default)
91
+ return create_model(model_name, **fields)
92
+
93
+
94
+ def _make_base_tool_class() -> type:
95
+ """Define the ``FlyteCrewAITool`` ``BaseTool`` subclass (imported lazily).
96
+
97
+ CrewAI is an optional heavy import, so the class is built on first use rather
98
+ than at module import time.
99
+ """
100
+ from crewai.tools import BaseTool
101
+
102
+ class FlyteCrewAITool(BaseTool):
103
+ """A CrewAI ``BaseTool`` backed by a Flyte task (or plain callable).
104
+
105
+ ``_run`` is synchronous by design: CrewAI's structured-tool path calls it
106
+ and ``asyncio.run``s any returned coroutine, which would fail inside a
107
+ Flyte task's running loop. We instead bridge to the async dispatcher via
108
+ :func:`run_sync` (a background-thread loop) and return a plain string.
109
+ """
110
+
111
+ # Pydantic model config: allow the non-field private attributes below.
112
+ _dispatch: typing.Callable[..., typing.Awaitable[typing.Any]]
113
+ _wrapped_task: typing.Any
114
+
115
+ def _run(self, **kwargs: typing.Any) -> str:
116
+ return run_sync(self._dispatch, **kwargs)
117
+
118
+ async def _arun(self, **kwargs: typing.Any) -> str:
119
+ return await self._dispatch(**kwargs)
120
+
121
+ @property
122
+ def __wrapped_task__(self) -> typing.Any:
123
+ return self._wrapped_task
124
+
125
+ @property
126
+ def task(self) -> typing.Any:
127
+ return self._wrapped_task
128
+
129
+ return FlyteCrewAITool
130
+
131
+
132
+ @functools.lru_cache(maxsize=1)
133
+ def _base_tool_class() -> type:
134
+ return _make_base_tool_class()
135
+
136
+
137
+ def _build_tool(
138
+ *,
139
+ tool_name: str,
140
+ desc: str,
141
+ args_model: type,
142
+ dispatch: typing.Callable[..., typing.Awaitable[typing.Any]],
143
+ wrapped_task: typing.Any,
144
+ ) -> typing.Any:
145
+ cls = _base_tool_class()
146
+ instance = cls(name=tool_name, description=desc, args_schema=args_model)
147
+ # Private attrs live outside pydantic validation; set them directly.
148
+ object.__setattr__(instance, "_dispatch", dispatch)
149
+ object.__setattr__(instance, "_wrapped_task", wrapped_task)
150
+ return instance
151
+
152
+
153
+ def _task_to_tool(
154
+ task: AsyncFunctionTaskTemplate,
155
+ *,
156
+ name: str | None = None,
157
+ description: str | None = None,
158
+ ) -> typing.Any:
159
+ """Build a CrewAI ``BaseTool`` from a Flyte task."""
160
+ tool_name = name or task.func.__name__
161
+ desc = (description or task.func.__doc__ or f"Run {tool_name}").strip()
162
+ task_json_schema(task) # validate schema at construction time
163
+ args_model = _args_model_from_signature(task.func, f"{tool_name}_args")
164
+
165
+ async def _dispatch(**kwargs: typing.Any) -> str:
166
+ # In a Flyte task context this submits a durable child action; locally it
167
+ # runs inline. ``coerce_tool_args`` relaxes LLM int->float args so Flyte's
168
+ # type engine doesn't reject e.g. ``amount_usd=42`` for a ``float`` param.
169
+ result = await task.aio(**coerce_tool_args(task, kwargs or {}))
170
+ return _as_content(result)
171
+
172
+ # Wire the shared resolver so the task resolves to itself on the worker.
173
+ attach_tool_resolver(task)
174
+
175
+ return _build_tool(
176
+ tool_name=tool_name,
177
+ desc=desc,
178
+ args_model=args_model,
179
+ dispatch=_dispatch,
180
+ wrapped_task=task,
181
+ )
182
+
183
+
184
+ def _callable_to_tool(
185
+ func: typing.Callable,
186
+ *,
187
+ name: str | None = None,
188
+ description: str | None = None,
189
+ ) -> typing.Any:
190
+ """Build a CrewAI ``BaseTool`` from a plain callable."""
191
+ tool_name = name or getattr(func, "__name__", "tool")
192
+ desc = (description or func.__doc__ or f"Run {tool_name}").strip()
193
+ NativeInterface.from_callable(func).json_schema # validate schema at construction time
194
+ args_model = _args_model_from_signature(func, f"{tool_name}_args")
195
+
196
+ async def _dispatch(**kwargs: typing.Any) -> str:
197
+ out = func(**(kwargs or {}))
198
+ if inspect.isawaitable(out):
199
+ out = await out
200
+ return _as_content(out)
201
+
202
+ return _build_tool(
203
+ tool_name=tool_name,
204
+ desc=desc,
205
+ args_model=args_model,
206
+ dispatch=_dispatch,
207
+ wrapped_task=None,
208
+ )
209
+
210
+
211
+ def _as_content(result: typing.Any) -> str:
212
+ """Convert a tool result to a string for CrewAI."""
213
+ if isinstance(result, str):
214
+ return result
215
+ try:
216
+ return json.dumps(result, default=str)
217
+ except (TypeError, ValueError):
218
+ return str(result)
219
+
220
+
221
+ def _coerce_tool(t: typing.Any) -> typing.Any:
222
+ """Coerce a tool to a CrewAI-compatible ``BaseTool``.
223
+
224
+ Bare ``@env.task`` templates are wrapped on the fly; already-wrapped tools
225
+ (``BaseTool`` instances) and other objects pass through unchanged.
226
+ """
227
+ if isinstance(t, AsyncFunctionTaskTemplate):
228
+ return tool(t)
229
+ return t
@@ -0,0 +1,9 @@
1
+ Metadata-Version: 2.4
2
+ Name: flyteplugins-agents-crewai
3
+ Version: 2.5.15
4
+ Summary: Run CrewAI agents on Flyte.
5
+ Author-email: Niels Bantilan <niels@union.ai>
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: flyteplugins-agents-core
9
+ Requires-Dist: crewai
@@ -0,0 +1,9 @@
1
+ flyteplugins/agents/crewai/__init__.py,sha256=A1LW9r9gjoreQxQmmh0YHX9V2XUW_ZE8vykTisWHQOI,598
2
+ flyteplugins/agents/crewai/_durable.py,sha256=f3onOoWpINXQoZVa0onwp7uCY-sgLEVBlPwhVeHzEyI,6738
3
+ flyteplugins/agents/crewai/_memory.py,sha256=CfcveVuJRIDBEt7B3SK_k2I5j0yI71LRlNIZQ8Kinsw,3041
4
+ flyteplugins/agents/crewai/_run.py,sha256=_tIw3juVDtd6UxYyAyBLv5SpJNI4ZE7xTw4jPJq8AUc,6236
5
+ flyteplugins/agents/crewai/_tools.py,sha256=YRJgraQn7IPh86JoX3Ln2RQTgc36i0I5WYefS7GcqNM,8641
6
+ flyteplugins_agents_crewai-2.5.15.dist-info/METADATA,sha256=vaonWVyB0GMQ_LT2H-TFXWyzbyM3TiIZIc5xBreYGHY,280
7
+ flyteplugins_agents_crewai-2.5.15.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
8
+ flyteplugins_agents_crewai-2.5.15.dist-info/top_level.txt,sha256=cgd779rPu9EsvdtuYgUxNHHgElaQvPn74KhB5XSeMBE,13
9
+ flyteplugins_agents_crewai-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