mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-08-10 08:21:54 +00:00
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
* feat(tools): surface tool failures instead of reporting them as success
A tool can finish without raising and still fail to do what it was asked.
Slack answers HTTP 200 with `{"ok": false, "error": "channel_not_found"}`;
an MCP server sets `isError`; a CrewAI AMP action returns
`API request failed: ...`. In every case the call "worked", so the error
text reached the agent as an ordinary result, the agent narrated the
problem in prose, and the run was recorded as a success.
Concretely: five failed `slackbot_send_message` calls each rendered as
"Tool Execution Completed", the task passed, and the crew passed -- with
the only evidence being a sentence in the final answer. Nothing
downstream could tell the difference, and an agent that keeps going on a
step that silently did nothing builds the rest of its work on it.
Give that outcome a type and a reaction:
- `ToolFailure` -- what a tool returns instead of an error string. The
agent still reads prose via `as_agent_message()`, so model behavior is
unchanged; the framework now knows the call failed.
- `ToolFailurePolicy` -- `ignore` (previous behavior), `warn` (default:
record + emit, keep going), `raise` (abort with
`ToolExecutionFailedError`). Resolved most-specific-first: tool, task,
agent, crew.
- `ToolFailureDetectedEvent` -- emitted before a `raise` aborts, so
subscribers always observe the failure. `ToolUsageFinishedEvent` also
carries a `failure` field so a trace UI can mark the call failed
without correlating two events.
- `tool_failures` on `TaskOutput`, `CrewOutput` and `LiteAgentOutput`,
plus `has_tool_failures`, so consumers never parse a string.
Detection is strictly declarative -- no string sniffing, so a tool that
legitimately returns text about an error is never misread as failing.
Failures come from a returned `ToolFailure`, a raised exception, MCP
`isError`, a spent `max_usage_count`, or an unknown tool.
Wired into all four tool-execution paths (the ReAct path and the three
native function-calling implementations). Sources updated to report
structurally: `MCPClient.call_tool_result()` preserves `isError` that
`call_tool()` dropped, and `CrewAIPlatformActionTool` returns a
`ToolFailure` for non-2xx and for caught exceptions.
Two latent bugs fixed along the way: `ToolUsage` assumed every agent has
a `fingerprint` (LiteAgent does not), and policy resolution now tolerates
malformed values rather than letting telemetry take down a tool call.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): address review round 1 on tool-failure signalling
Five real defects from Bugbot, none of them cosmetic.
Tool-scoped policy never applied (high). `resolve_tool_failure_policy`
read `tool_failure_policy` off the object handed to it, but every
execution path passes the `CrewStructuredTool` wrapper, which never
carried the attribute -- and `BaseTool` never declared it in the first
place. A tool-scoped `raise`/`ignore` was silently ignored while the
docs and a unit test claimed otherwise; the test passed only because it
called the resolver directly with an authored tool. Declared the field on
`BaseTool`, propagated it through `to_structured_tool()` and
`CrewStructuredTool`, and made resolution fall back through
`_original_tool` so either shape works.
A failed call still printed the green "Completed" panel, then the red
one. That is the terminal version of the exact bug this PR is about.
Suppressed the success panel when the call reported failure.
A raised tool printed twice: `ToolUsageErrorEvent` already renders a red
panel, and the new failure panel repeated it. The event is still emitted
-- policy and traces need it -- but the duplicate console output is gone.
Both decisions now live in named predicates on `ConsoleFormatter` rather
than inline in the listener closure, so they are directly testable.
Unknown tools were reported on the ReAct path but silently ignored on all
three native paths, so the same miss was loud or silent depending on
executor style. Native paths now record `UNKNOWN_TOOL` too. This also
surfaced a live `NameError`: ruff had pruned `ToolFailureReason` from
`agent_utils` as unused, so the new branch would have crashed at runtime.
`LiteAgentOutput` had `tool_failures` but not `has_tool_failures`, which
the PR promised on all three output types -- an `AttributeError` for any
caller sharing one check across result types.
Testing: 16 further tests, 45 total. Two console tests were passing
vacuously because `emit()` dispatches sync handlers on a thread pool, so
the assertions raced the handler; they now assert on the predicates
directly, and the native-path test drains the bus with `flush()` and
checks the synchronously-written record. Full suite still matches
baseline exactly at 377 pre-existing failures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* chore: update tool specifications
* fix(tools): address review round 2 and fix CI type failure
CI caught a type error I should have: widening `agent` to accept a
`LiteAgent` (so a standalone LiteAgent resolves its own policy) left the
declared signatures behind. Widened `execute_tool_and_check_finality`,
its async twin, and `ToolCallHookContext` to `Agent | BaseAgent |
LiteAgent | None`, which is what those actually receive now.
Seven CodeRabbit findings, all verified against the code first:
`raise` was being downgraded by three enclosing handlers. With
`max_execution_time` set, `_execute_with_timeout` wrapped every exception
in `RuntimeError`, so `_check_execution_error` no longer recognized the
passthrough and sent the task through the retry loop instead of aborting.
`StepExecutor.execute` turned it into `StepResult(success=False)` and let
the plan continue. `LiteAgent.kickoff` ran it through
`handle_unknown_error` and printed "This is likely a bug - please report
it" for what is a deliberate, configured stop.
Failure records were dropped on two paths. `reset_tool_failures()` only
ran in `_prepare_task_execution`, so `Agent.kickoff()` / `kickoff_async()`
— which enter through `_prepare_kickoff` — accumulated records across
runs. And a guardrail retry calls `execute_task` again, which resets the
agent, so a tool that failed on a blocked attempt vanished from the final
output entirely: a run could report zero failures having demonstrably
failed one. Failures now accumulate across guardrail attempts.
Writing the tests for that surfaced a further miss of my own:
`Agent.kickoff()` builds its `LiteAgentOutput` in `agent/core.py` via
`AgentExecutor`, not through `LiteAgent`, so `tool_failures` was always
empty there regardless of the recording fix. Wired up, and the LiteAgent
path now reads from whichever agent the executor was handed
(`original_agent` under kickoff, `self` standalone) rather than assuming.
`last_tool_failures` returns a copy, so a caller cannot mutate the
agent's record or watch it shift mid-run.
Testing: 7 further tests, 52 total, covering the timeout wrapper, the
retry limit, kickoff reset, the kickoff output path, copy semantics and
guardrail accumulation. Full suite matches baseline exactly at 377
pre-existing failures; mypy clean on every changed file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): make crew-scoped policy real and close the last raise leak
Two findings, and the first was a documented feature that never worked.
`resolve_tool_failure_policy` consulted a crew, and the docs advertised
crew as a scope, but `Crew` had no `tool_failure_policy` field at all --
and even with one it was unreachable, because `BaseAgent` defaulted the
policy to `WARN` rather than `None`, so resolution always stopped at the
agent. Crew-level configuration was silently ignored.
Fixed by making "inherit" the default everywhere instead of baking `warn`
into one layer: `Crew` gains the field, and `BaseAgent`/`LiteAgent`
default to `None` like `Task` and `BaseTool` already did. The resolver
owns the single fallback, so the chain is genuinely
tool > task > agent > crew > warn and the effective default with nothing
configured is still `warn`. Reading `agent.tool_failure_policy` now
returns `None` (meaning "inherit") rather than `WARN`.
The other: `StepExecutor` re-raised `ToolExecutionFailedError` from its
outer handler, but the nested handler around the native-to-text tooling
fallback still caught it and returned `StepResult(success=False)`. An
agent whose LLM lacked native tool calling would therefore not abort
under `raise`. That is the third distinct place this exception was being
downgraded; it now re-raises there too.
Testing: 8 further tests, 60 total, including the full precedence chain
walked one level at a time and crew-scoped `raise`/`ignore` driven
end-to-end through `kickoff()` rather than only through the resolver --
the gap that let the original crew bug pass review. Full suite matches
baseline exactly at 377 pre-existing failures; mypy clean on every
changed file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* docs: trim comments and docstrings on tool-failure signalling
Prose only -- no behavior change. Cut the module docstring, the longer
class and method docstrings, the multi-line inline comments, and the
verbose Field descriptions down to what actually earns its place. Net 87
lines lighter.
Kept the "why" in every case where the reason is non-obvious (why the
event fires before a raise, why the policy reads through the tool wrapper,
why the bus needs draining in tests) and dropped the restatements of what
the code already says.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): make ignore truly silent, stop caching failures, close 4 gaps
Six findings from the latest review round, all verified against the code
before touching it.
`ignore` was not silent. `ToolUsageFinishedEvent.failure` was set before
the policy ran, so traces still saw a failed call under a policy documented
as surfacing nothing. Worse, the console then showed *no* panel at all:
green was suppressed because `failure` was present, red was skipped because
`ignore` never emits `ToolFailureDetectedEvent`. New `reportable_failure()`
resolves the policy before the finished event and drops the flag under
`ignore`; wired into all four execution paths.
Failures were being cached. `CacheHandler.add` stored a `ToolFailure` like
any other result, so a transient error became permanent for the rest of the
run and every later hit re-reported a call that never re-ran. The cache now
refuses to store declared failures -- fixed at the single choke point rather
than at each of the four call sites.
A spent `max_usage_count` was invisible on the shared native path.
`BaseTool._claim_usage` returned a bare string that only the executors
recognising that exact message treated as a failure. It now returns a
`ToolFailure` with `USAGE_LIMIT`, so every path records it.
A guardrail returning a whole `TaskOutput` replaced the output without
carrying accumulated failures over, so earlier attempts vanished from
`CrewOutput.tool_failures`. New `merge_tool_failures()` combines and
deduplicates, and the retry-rebuild path uses it too.
A hook-blocked call inherited a cached failure and attributed it to a call
that never ran. Now cleared. Not reachable through the built-in cache once
failures stop being cached, so the test injects a custom cache handler that
does retain them -- verified to fail without the guard.
Also removed a `datetime` import left unused by the earlier console-test
rewrite.
Testing: 13 further tests, 73 total. Full suite matches baseline exactly at
377 pre-existing failures; the usage-limit suites that `_claim_usage`
touches pass unchanged; mypy clean on every changed file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): let raise through the parallel native path, guard all handlers
Chasing down CodeRabbit's note about callers of
execute_single_native_tool_call turned up a fifth place this exception was
being downgraded: the experimental executor's parallel branch wrapped
future.result() in a broad except and folded the abort into a fake tool
result, so the remaining parallel calls carried on. The sequential path and
crew_agent_executor's parallel branch were already fine.
Five separate handlers have swallowed this during review, so added a guard
test asserting the passthrough at every site rather than trusting the next
one gets spotted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): keep a failed tool out of the final answer, finish crew scope
Three more findings, all confirmed against the code.
A failed `result_as_answer` tool still became the task's output. The native
paths already excluded raised errors and hook blocks from short-circuiting,
but not declared failures -- so an error message silently became the answer,
which is the exact shape of bug this PR exists to prevent. Fixed on all
paths, and there were three independent override points, not one:
`ToolResult.result_as_answer` in tool_utils, the `execution_result`
finality checks in both executors, and `process_tool_results()`, which
reads `agent.tools_results` back separately. The first two fixes alone left
the behavior unchanged; only the third made the test pass.
`ToolUsage` never received a crew, so a crew-level `ignore` half-applied:
recording and `ToolFailureDetectedEvent` stayed quiet, but the flag was
still attached to `ToolUsageFinishedEvent`. It now takes and stores `crew`.
`CrewAgentExecutor.invoke`/`ainvoke` routed a deliberate stop through
`handle_unknown_error`, printing "An unknown error occurred" on verbose
runs. LiteAgent already special-cased this; both now do.
Testing: 5 further tests, 79 total, including that a *successful*
`result_as_answer` tool still short-circuits. Full suite matches baseline
exactly at 377 pre-existing failures; mypy clean on every changed file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): report malformed tool args, correlate the failure event
Two findings from the latest round.
Malformed native tool arguments returned early with a plain error dict and
never reported a failure, so `ToolFailureReason.INVALID_INPUT` was declared
but unreferenced -- a bad tool call was absent from records, events and
`raise` aborts. `parse_tool_call_args` now carries an INVALID_INPUT failure
on the error dict and both executors report it before returning.
`ToolFailureDetectedEvent` never set `agent_id`, so a trace could not tie it
to a specific agent instance. Fixing that exposed the same gap running the
other way: `ToolUsage`'s own started/finished/error events never set
`agent_id` either, so on the ReAct path the paired finished event had
nothing to correlate against. Both now set it.
Set explicitly rather than via `from_agent`, which would also overwrite
`agent_role` and lose the `_original_role` preference those events already
apply -- a behavior change that has nothing to do with correlation.
Testing: 5 further tests, 84 total, asserting the ids match between the
failure event and its paired finished event. One existing test pinned the
exact key set of the parse-error dict and was updated for the new key. Full
suite matches baseline at 377 pre-existing failures; the one apparent
addition was the known `test_trace_enable_disable` order-flake, confirmed by
re-running rather than assumed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): scope failure accumulation per execution, drop deprecated executor
Two review requests from @lorenzejay.
Accumulation no longer lives as mutable state on the shared agent. A
ContextVar collector is opened around each execution -- task, kickoff, and
each guardrail retry -- and the output reads that collector directly instead
of copying the agent's list. ContextVars are copied per asyncio task and per
thread, so concurrent executions cannot see each other's records, and
nesting is safe for retries. `last_tool_failures` prefers the active
collector and falls back to the last completed execution, so the accessor is
correct during a run too. The per-execution reset that caused the erasure is
gone.
Reproducing this took some digging and the finding is worth recording: crew
tasks *cannot* hit it, because `AgentExecutor` refuses concurrent reuse of
one instance and raises. `agent.kickoff()` has no such guard, and there the
bug reproduces exactly as reported -- two concurrent kickoffs each returned
two records. The regression test forces the overlap with a barrier so it is
deterministic rather than timing-dependent, and I verified it reports [2, 2]
against the old behavior and [1, 1] now.
Removed the tool-failure integration from `CrewAgentExecutor` entirely; that
file is back to its state on main. Note the shared ReAct helper it calls
still records failures, since that is common code rather than new behavior in
the deprecated file -- so a `raise` policy will be swallowed by that
executor's generic handler. Flagged on the PR rather than papered over.
Testing: 89 total. Two tests I wrote for this were vacuous on the first
attempt -- they passed against the simulated pre-fix code -- so each
concurrency test was checked against the old behavior before being kept.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): report malformed calls everywhere, drop the unused block reason
Four findings.
`execute_single_native_tool_call` swallowed a JSON decode error into an empty
args dict and ran the tool with no input at all -- worse than not reporting
it. It now routes through `parse_tool_call_args` like the executors do, so
the StepExecutor/planning path reports INVALID_INPUT and returns instead of
executing. That also removes a duplicated inline parse.
The ReAct path returned a `ToolUsageError` message as an ordinary result
without reporting it, so a malformed call there was invisible while the
equivalent native failure was recorded. Now reported as INVALID_INPUT too.
`Agent.kickoff` opened a collector but no longer reset the agent-level list,
so `last_tool_failures` grew across kickoffs. Reset restored, matching task
execution.
`ToolFailureReason.BLOCKED_BY_HOOK` was declared and never produced. Rather
than start reporting hook blocks as failures, the member is removed: a block
is a deliberate decision by the hook author, and treating it as a failure
would make `raise` abort on an intentional veto. Added a guard test that every
remaining reason is actually produced somewhere, so a dead member cannot
reappear -- the same smell that flagged INVALID_INPUT last round.
Also switched the deprecation guard test to a single import style.
Testing: 6 further tests, 95 total, including that the tool does not run when
its args fail to parse. Full suite matches baseline at 377 pre-existing
failures; mypy clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
* fix(tools): merge failures across kickoff guardrail retries, cancel siblings
Kickoff guardrail retries discarded the blocked attempt's failures. Each
retry calls `_execute_and_build_output`, which opens a fresh collector and
builds a new output, so a run could report zero failures having demonstrably
failed one -- the same bug already fixed on the task guardrail path, which
merges. Now merged there too. Verified the test fails without the fix.
Under `raise`, one parallel native tool aborting left its siblings running:
the pool waited for them and pending ones still started. It now shuts the
pool down with `cancel_futures=True` so a not-yet-started sibling never runs.
Threads already in flight cannot be interrupted in Python, so a concurrent
tool may still complete before the abort surfaces; that is noted at the call
site rather than left implied.
Also satisfied CodeQL by materialising the enum in the guard test's loop.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ETacm2dMASfpMAYUiDu5YG
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com>
1725 lines
55 KiB
Python
1725 lines
55 KiB
Python
"""Test Agent creation and execution basic functionality."""
|
|
|
|
import ast
|
|
import json
|
|
import os
|
|
import time
|
|
from functools import partial
|
|
from hashlib import md5
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from pydantic import BaseModel
|
|
from pydantic_core import ValidationError
|
|
|
|
from crewai import Agent, Crew, Process, Task
|
|
from crewai.tasks.conditional_task import ConditionalTask
|
|
from crewai.tasks.task_output import TaskOutput
|
|
from crewai.utilities.converter import Converter
|
|
from crewai.utilities.string_utils import interpolate_only
|
|
|
|
|
|
def test_task_tool_reflect_agent_tools():
|
|
from crewai.tools import tool
|
|
|
|
@tool
|
|
def fake_tool() -> None:
|
|
"Fake tool"
|
|
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
tools=[fake_tool],
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 ideas.",
|
|
agent=researcher,
|
|
)
|
|
|
|
assert task.tools == [fake_tool]
|
|
|
|
|
|
def test_task_tool_takes_precedence_over_agent_tools():
|
|
from crewai.tools import tool
|
|
|
|
@tool
|
|
def fake_tool() -> None:
|
|
"Fake tool"
|
|
|
|
@tool
|
|
def fake_task_tool() -> None:
|
|
"Fake tool"
|
|
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
tools=[fake_tool],
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for an article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 ideas.",
|
|
agent=researcher,
|
|
tools=[fake_task_tool],
|
|
)
|
|
|
|
assert task.tools == [fake_task_tool]
|
|
|
|
|
|
def test_task_prompt_includes_expected_output():
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
agent=researcher,
|
|
)
|
|
|
|
with patch.object(Agent, "execute_task") as execute:
|
|
execute.return_value = "ok"
|
|
task.execute_sync(agent=researcher)
|
|
execute.assert_called_once_with(task=task, context=None, tools=[])
|
|
|
|
|
|
def test_task_callback():
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task_completed = MagicMock(return_value="done")
|
|
|
|
task = Task(
|
|
name="Brainstorm",
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
agent=researcher,
|
|
callback=task_completed,
|
|
)
|
|
|
|
with patch.object(Agent, "execute_task") as execute:
|
|
execute.return_value = "ok"
|
|
task.execute_sync(agent=researcher)
|
|
task_completed.assert_called_once_with(task.output)
|
|
|
|
assert task.output.description == task.description
|
|
assert task.output.expected_output == task.expected_output
|
|
assert task.output.name == task.name
|
|
|
|
|
|
def test_task_callback_returns_task_output():
|
|
from crewai.tasks.output_format import OutputFormat
|
|
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task_completed = MagicMock(return_value="done")
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for an article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
agent=researcher,
|
|
callback=task_completed,
|
|
)
|
|
|
|
with patch.object(Agent, "execute_task") as execute:
|
|
execute.return_value = "exported_ok"
|
|
task.execute_sync(agent=researcher)
|
|
# Ensure the callback is called with a TaskOutput object serialized to JSON
|
|
task_completed.assert_called_once()
|
|
callback_data = task_completed.call_args[0][0]
|
|
|
|
if isinstance(callback_data, TaskOutput):
|
|
callback_data = json.dumps(callback_data.model_dump())
|
|
|
|
assert isinstance(callback_data, str)
|
|
output_dict = json.loads(callback_data)
|
|
expected_output = {
|
|
"description": task.description,
|
|
"raw": "exported_ok",
|
|
"pydantic": None,
|
|
"json_dict": None,
|
|
"agent": researcher.role,
|
|
"summary": "Give me a list of 5 interesting ideas to explore...",
|
|
"name": task.name or task.description,
|
|
"expected_output": "Bullet point list of 5 interesting ideas.",
|
|
"output_format": OutputFormat.RAW,
|
|
"messages": [],
|
|
"tool_failures": [],
|
|
}
|
|
assert output_dict == expected_output
|
|
|
|
|
|
def test_execute_with_agent():
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
)
|
|
|
|
with patch.object(Agent, "execute_task", return_value="ok") as execute:
|
|
task.execute_sync(agent=researcher)
|
|
execute.assert_called_once_with(task=task, context=None, tools=[])
|
|
|
|
|
|
def test_async_execution():
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
async_execution=True,
|
|
agent=researcher,
|
|
)
|
|
|
|
with patch.object(Agent, "execute_task", return_value="ok") as execute:
|
|
execution = task.execute_async(agent=researcher)
|
|
result = execution.result()
|
|
assert result.raw == "ok"
|
|
execute.assert_called_once_with(task=task, context=None, tools=[])
|
|
|
|
|
|
def test_multiple_output_type_error():
|
|
class Output(BaseModel):
|
|
field: str
|
|
|
|
with pytest.raises(ValidationError):
|
|
Task(
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
output_json=Output,
|
|
output_pydantic=Output,
|
|
)
|
|
|
|
|
|
def test_guardrail_type_error():
|
|
desc = "Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting."
|
|
expected_output = "Bullet point list of 5 interesting ideas."
|
|
# Lambda function
|
|
Task(
|
|
description=desc,
|
|
expected_output=expected_output,
|
|
guardrail=lambda x: (True, x),
|
|
)
|
|
|
|
# Function
|
|
def guardrail_fn(x: TaskOutput) -> tuple[bool, TaskOutput]:
|
|
return (True, x)
|
|
|
|
Task(
|
|
description=desc,
|
|
expected_output=expected_output,
|
|
guardrail=guardrail_fn,
|
|
)
|
|
|
|
class Object:
|
|
def guardrail_fn(self, x: TaskOutput) -> tuple[bool, TaskOutput]:
|
|
return (True, x)
|
|
|
|
@classmethod
|
|
def guardrail_class_fn(cls, x: TaskOutput) -> tuple[bool, str]:
|
|
return (True, x)
|
|
|
|
@staticmethod
|
|
def guardrail_static_fn(x: TaskOutput) -> tuple[bool, str | TaskOutput]:
|
|
return (True, x)
|
|
|
|
obj = Object()
|
|
# Method
|
|
Task(
|
|
description=desc,
|
|
expected_output=expected_output,
|
|
guardrail=obj.guardrail_fn,
|
|
)
|
|
Task(
|
|
description=desc,
|
|
expected_output=expected_output,
|
|
guardrail=Object.guardrail_class_fn,
|
|
)
|
|
# Static method
|
|
Task(
|
|
description=desc,
|
|
expected_output=expected_output,
|
|
guardrail=Object.guardrail_static_fn,
|
|
)
|
|
|
|
def error_fn(x: TaskOutput, y: bool) -> tuple[bool, TaskOutput]:
|
|
return (y, x)
|
|
|
|
Task(
|
|
description=desc,
|
|
expected_output=expected_output,
|
|
guardrail=partial(error_fn, y=True),
|
|
)
|
|
|
|
with pytest.raises(ValidationError):
|
|
Task(
|
|
description=desc,
|
|
expected_output=expected_output,
|
|
guardrail=error_fn,
|
|
)
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_output_pydantic_sequential():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_pydantic=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task], process=Process.sequential)
|
|
result = crew.kickoff()
|
|
assert isinstance(result.pydantic, ScoreOutput)
|
|
assert result.to_dict() == {"score": 4}
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_output_pydantic_hierarchical():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_pydantic=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[scorer],
|
|
tasks=[task],
|
|
process=Process.hierarchical,
|
|
manager_llm="gpt-4o",
|
|
)
|
|
result = crew.kickoff()
|
|
assert isinstance(result.pydantic, ScoreOutput)
|
|
assert result.to_dict() == {"score": 4}
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_output_json_sequential():
|
|
import uuid
|
|
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
output_file = f"score_{uuid.uuid4()}.json"
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_json=ScoreOutput,
|
|
output_file=output_file,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task], process=Process.sequential)
|
|
result = crew.kickoff()
|
|
assert '{"score": 4}' == result.json
|
|
assert result.to_dict() == {"score": 4}
|
|
|
|
if os.path.exists(output_file):
|
|
os.remove(output_file)
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_output_json_hierarchical():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_json=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[scorer],
|
|
tasks=[task],
|
|
process=Process.hierarchical,
|
|
manager_llm="gpt-4o",
|
|
)
|
|
result = crew.kickoff()
|
|
assert result.json == '{"score": 4}'
|
|
assert result.to_dict() == {"score": 4}
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_inject_date():
|
|
reporter = Agent(
|
|
role="Reporter",
|
|
goal="Report the date",
|
|
backstory="You're an expert reporter, specialized in reporting the date.",
|
|
allow_delegation=False,
|
|
inject_date=True,
|
|
)
|
|
|
|
task = Task(
|
|
description="What is the date today?",
|
|
expected_output="The date today as you were told, same format as the date you were told.",
|
|
agent=reporter,
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[reporter],
|
|
tasks=[task],
|
|
process=Process.sequential,
|
|
)
|
|
result = crew.kickoff()
|
|
assert "2025-05-21" in result.raw
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_inject_date_custom_format():
|
|
reporter = Agent(
|
|
role="Reporter",
|
|
goal="Report the date",
|
|
backstory="You're an expert reporter, specialized in reporting the date.",
|
|
allow_delegation=False,
|
|
inject_date=True,
|
|
date_format="%B %d, %Y",
|
|
)
|
|
|
|
task = Task(
|
|
description="What is the date today?",
|
|
expected_output="The date today.",
|
|
agent=reporter,
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[reporter],
|
|
tasks=[task],
|
|
process=Process.sequential,
|
|
)
|
|
result = crew.kickoff()
|
|
assert "May 21, 2025" in result.raw
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_no_inject_date():
|
|
reporter = Agent(
|
|
role="Reporter",
|
|
goal="Report the date",
|
|
backstory="You're an expert reporter, specialized in reporting the date.",
|
|
allow_delegation=False,
|
|
inject_date=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="What is the date today?",
|
|
expected_output="The date today.",
|
|
agent=reporter,
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[reporter],
|
|
tasks=[task],
|
|
process=Process.sequential,
|
|
)
|
|
result = crew.kickoff()
|
|
assert "2025-05-21" not in result.raw
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_json_property_without_output_json():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_pydantic=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task], process=Process.sequential)
|
|
result = crew.kickoff()
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
_ = result.json # Attempt to access the json property
|
|
|
|
assert "No JSON output found in the final task." in str(excinfo.value)
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_output_json_dict_sequential():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_json=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task], process=Process.sequential)
|
|
result = crew.kickoff()
|
|
assert {"score": 4} == result.json_dict
|
|
assert result.to_dict() == {"score": 4}
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_output_json_dict_hierarchical():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_json=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[scorer],
|
|
tasks=[task],
|
|
process=Process.hierarchical,
|
|
manager_llm="gpt-4o",
|
|
)
|
|
result = crew.kickoff()
|
|
assert {"score": 4} == result.json_dict
|
|
assert result.to_dict() == {"score": 4}
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_output_pydantic_to_another_task():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
llm="gpt-4o",
|
|
function_calling_llm="gpt-4o",
|
|
verbose=True,
|
|
)
|
|
|
|
task1 = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_pydantic=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
task2 = Task(
|
|
description="Given the score the title 'The impact of AI in the future of work' got, give me an integer score between 1-5 for the following title: 'Return of the Jedi', you MUST give it a score, use your best judgment",
|
|
expected_output="The score of the title.",
|
|
output_pydantic=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task1, task2], verbose=True)
|
|
result = crew.kickoff()
|
|
pydantic_result = result.pydantic
|
|
assert isinstance(pydantic_result, ScoreOutput), (
|
|
"Expected pydantic result to be of type ScoreOutput"
|
|
)
|
|
assert pydantic_result.score == 5
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_output_json_to_another_task():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task1 = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_json=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
task2 = Task(
|
|
description="Given the score the title 'The impact of AI in the future of work' got, give me an integer score between 1-5 for the following title: 'Return of the Jedi'",
|
|
expected_output="The score of the title.",
|
|
output_json=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task1, task2])
|
|
result = crew.kickoff()
|
|
assert '{"score": 3}' == result.json
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_save_task_output():
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_file="score.json",
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task])
|
|
|
|
with patch.object(Task, "_save_file") as save_file:
|
|
save_file.return_value = None
|
|
crew.kickoff()
|
|
save_file.assert_called_once()
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_save_task_json_output():
|
|
from unittest.mock import patch
|
|
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_file="score.json",
|
|
output_json=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task])
|
|
|
|
# Mock only the _save_file method to avoid actual file I/O
|
|
with patch.object(Task, "_save_file") as mock_save:
|
|
result = crew.kickoff()
|
|
assert result is not None
|
|
mock_save.assert_called_once()
|
|
|
|
call_args = mock_save.call_args
|
|
if call_args:
|
|
saved_content = call_args[0][0]
|
|
if isinstance(saved_content, str):
|
|
data = json.loads(saved_content)
|
|
assert "score" in data
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_save_task_pydantic_output(tmp_path, monkeypatch):
|
|
"""Test saving pydantic output to a file.
|
|
|
|
Uses tmp_path fixture and monkeypatch to change directory to avoid
|
|
file system race conditions on enterprise systems.
|
|
"""
|
|
from pathlib import Path
|
|
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
output_file = "score_output.json"
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_file=output_file,
|
|
output_pydantic=ScoreOutput,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task])
|
|
crew.kickoff()
|
|
|
|
output_path = Path(output_file).resolve()
|
|
assert output_path.exists()
|
|
assert {"score": 4} == json.loads(output_path.read_text())
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_custom_converter_cls():
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
class ScoreConverter(Converter):
|
|
pass
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
output_pydantic=ScoreOutput,
|
|
converter_cls=ScoreConverter,
|
|
agent=scorer,
|
|
)
|
|
|
|
crew = Crew(agents=[scorer], tasks=[task])
|
|
|
|
# so the converter is bypassed. Verify the output is valid instead.
|
|
result = crew.kickoff()
|
|
assert isinstance(result.pydantic, ScoreOutput)
|
|
assert isinstance(result.pydantic.score, int)
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_increment_delegations_for_hierarchical_process():
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[scorer],
|
|
tasks=[task],
|
|
process=Process.hierarchical,
|
|
manager_llm="gpt-4o",
|
|
)
|
|
|
|
with patch.object(Task, "increment_delegations") as increment_delegations:
|
|
increment_delegations.return_value = None
|
|
crew.kickoff()
|
|
increment_delegations.assert_called_once()
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_increment_delegations_for_sequential_process():
|
|
manager = Agent(
|
|
role="Manager",
|
|
goal="Coordinate scoring processes",
|
|
backstory="You're great at delegating work about scoring.",
|
|
allow_delegation=True,
|
|
)
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
allow_delegation=True,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'",
|
|
expected_output="The score of the title.",
|
|
agent=manager,
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[manager, scorer],
|
|
tasks=[task],
|
|
process=Process.sequential,
|
|
)
|
|
|
|
with patch.object(Task, "increment_delegations") as increment_delegations:
|
|
increment_delegations.return_value = None
|
|
crew.kickoff()
|
|
increment_delegations.assert_called_once()
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_increment_tool_errors():
|
|
from crewai.tools import tool
|
|
|
|
@tool
|
|
def scoring_examples() -> None:
|
|
"Useful examples for scoring titles."
|
|
raise Exception("Error")
|
|
|
|
scorer = Agent(
|
|
role="Scorer",
|
|
goal="Score the title",
|
|
backstory="You're an expert scorer, specialized in scoring titles.",
|
|
tools=[scoring_examples],
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work', check examples to based your evaluation.",
|
|
expected_output="The score of the title.",
|
|
)
|
|
|
|
crew = Crew(
|
|
agents=[scorer],
|
|
tasks=[task],
|
|
process=Process.hierarchical,
|
|
manager_llm="gpt-4-0125-preview",
|
|
)
|
|
|
|
with patch.object(Task, "increment_tools_errors") as increment_tools_errors:
|
|
increment_tools_errors.return_value = None
|
|
crew.kickoff()
|
|
assert len(increment_tools_errors.mock_calls) > 0
|
|
|
|
|
|
def test_task_definition_based_on_dict():
|
|
config = {
|
|
"description": "Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work', check examples to based your evaluation.",
|
|
"expected_output": "The score of the title.",
|
|
}
|
|
|
|
task = Task(**config)
|
|
|
|
assert task.description == config["description"]
|
|
assert task.expected_output == config["expected_output"]
|
|
assert task.agent is None
|
|
|
|
|
|
def test_conditional_task_definition_based_on_dict():
|
|
config = {
|
|
"description": "Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work', check examples to based your evaluation.",
|
|
"expected_output": "The score of the title.",
|
|
}
|
|
|
|
task = ConditionalTask(**config, condition=lambda x: True)
|
|
|
|
assert task.description == config["description"]
|
|
assert task.expected_output == config["expected_output"]
|
|
assert task.agent is None
|
|
|
|
|
|
def test_conditional_task_copy_preserves_type():
|
|
task_config = {
|
|
"description": "Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work', check examples to based your evaluation.",
|
|
"expected_output": "The score of the title.",
|
|
}
|
|
original_task = Task(**task_config)
|
|
copied_task = original_task.copy(agents=[], task_mapping={})
|
|
assert isinstance(copied_task, Task)
|
|
|
|
original_conditional_config = {
|
|
"description": "Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'. Check examples to base your evaluation on.",
|
|
"expected_output": "The score of the title.",
|
|
"condition": lambda x: True,
|
|
}
|
|
original_conditional_task = ConditionalTask(**original_conditional_config)
|
|
copied_conditional_task = original_conditional_task.copy(agents=[], task_mapping={})
|
|
assert isinstance(copied_conditional_task, ConditionalTask)
|
|
|
|
|
|
def test_interpolate_inputs(tmp_path):
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas about {topic} to explore for an article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas about {topic}.",
|
|
output_file=str(tmp_path / "{topic}" / "output_{date}.txt"),
|
|
)
|
|
|
|
task.interpolate_inputs_and_add_conversation_history(
|
|
inputs={"topic": "AI", "date": "2025"}
|
|
)
|
|
assert (
|
|
task.description
|
|
== "Give me a list of 5 interesting ideas about AI to explore for an article, what makes them unique and interesting."
|
|
)
|
|
assert task.expected_output == "Bullet point list of 5 interesting ideas about AI."
|
|
assert task.output_file == str(tmp_path / "AI" / "output_2025.txt")
|
|
|
|
task.interpolate_inputs_and_add_conversation_history(
|
|
inputs={"topic": "ML", "date": "2025"}
|
|
)
|
|
assert (
|
|
task.description
|
|
== "Give me a list of 5 interesting ideas about ML to explore for an article, what makes them unique and interesting."
|
|
)
|
|
assert task.expected_output == "Bullet point list of 5 interesting ideas about ML."
|
|
assert task.output_file == str(tmp_path / "ML" / "output_2025.txt")
|
|
|
|
|
|
def test_interpolate_only():
|
|
"""Test the interpolate_only method for various scenarios including JSON structure preservation."""
|
|
|
|
json_string = '{"info": "Look at {placeholder}", "nested": {"val": "{nestedVal}"}}'
|
|
result = interpolate_only(
|
|
input_string=json_string,
|
|
inputs={"placeholder": "the data", "nestedVal": "something else"},
|
|
)
|
|
assert '"info": "Look at the data"' in result
|
|
assert '"val": "something else"' in result
|
|
assert "{placeholder}" not in result
|
|
assert "{nestedVal}" not in result
|
|
|
|
normal_string = "Hello {name}, welcome to {place}!"
|
|
result = interpolate_only(
|
|
input_string=normal_string, inputs={"name": "John", "place": "CrewAI"}
|
|
)
|
|
assert result == "Hello John, welcome to CrewAI!"
|
|
|
|
result = interpolate_only(input_string="", inputs={"unused": "value"})
|
|
assert result == ""
|
|
|
|
no_placeholders = "Hello, this is a test"
|
|
result = interpolate_only(input_string=no_placeholders, inputs={"unused": "value"})
|
|
assert result == no_placeholders
|
|
|
|
|
|
def test_interpolate_only_with_dict_inside_expected_output():
|
|
"""Test the interpolate_only method for various scenarios including JSON structure preservation."""
|
|
|
|
json_string = '{"questions": {"main_question": "What is the user\'s name?", "secondary_question": "What is the user\'s age?"}}'
|
|
result = interpolate_only(
|
|
input_string=json_string,
|
|
inputs={
|
|
"questions": {
|
|
"main_question": "What is the user's name?",
|
|
"secondary_question": "What is the user's age?",
|
|
}
|
|
},
|
|
)
|
|
assert '"main_question": "What is the user\'s name?"' in result
|
|
assert '"secondary_question": "What is the user\'s age?"' in result
|
|
assert result == json_string
|
|
|
|
normal_string = "Hello {name}, welcome to {place}!"
|
|
result = interpolate_only(
|
|
input_string=normal_string, inputs={"name": "John", "place": "CrewAI"}
|
|
)
|
|
assert result == "Hello John, welcome to CrewAI!"
|
|
|
|
result = interpolate_only(input_string="", inputs={"unused": "value"})
|
|
assert result == ""
|
|
|
|
no_placeholders = "Hello, this is a test"
|
|
result = interpolate_only(input_string=no_placeholders, inputs={"unused": "value"})
|
|
assert result == no_placeholders
|
|
|
|
|
|
def test_task_output_str_with_pydantic():
|
|
from crewai.tasks.output_format import OutputFormat
|
|
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
score_output = ScoreOutput(score=4)
|
|
task_output = TaskOutput(
|
|
description="Test task",
|
|
agent="Test Agent",
|
|
pydantic=score_output,
|
|
output_format=OutputFormat.PYDANTIC,
|
|
)
|
|
|
|
assert str(task_output) == str(score_output)
|
|
|
|
|
|
def test_task_output_str_with_json_dict():
|
|
from crewai.tasks.output_format import OutputFormat
|
|
|
|
json_dict = {"score": 4}
|
|
task_output = TaskOutput(
|
|
description="Test task",
|
|
agent="Test Agent",
|
|
json_dict=json_dict,
|
|
output_format=OutputFormat.JSON,
|
|
)
|
|
|
|
assert str(task_output) == str(json_dict)
|
|
|
|
|
|
def test_task_output_str_with_raw():
|
|
from crewai.tasks.output_format import OutputFormat
|
|
|
|
raw_output = "Raw task output"
|
|
task_output = TaskOutput(
|
|
description="Test task",
|
|
agent="Test Agent",
|
|
raw=raw_output,
|
|
output_format=OutputFormat.RAW,
|
|
)
|
|
|
|
assert str(task_output) == raw_output
|
|
|
|
|
|
def test_task_output_str_with_pydantic_and_json_dict():
|
|
from crewai.tasks.output_format import OutputFormat
|
|
|
|
class ScoreOutput(BaseModel):
|
|
score: int
|
|
|
|
score_output = ScoreOutput(score=4)
|
|
json_dict = {"score": 4}
|
|
task_output = TaskOutput(
|
|
description="Test task",
|
|
agent="Test Agent",
|
|
pydantic=score_output,
|
|
json_dict=json_dict,
|
|
output_format=OutputFormat.PYDANTIC,
|
|
)
|
|
|
|
# When both pydantic and json_dict are present, pydantic should take precedence
|
|
assert str(task_output) == str(score_output)
|
|
|
|
|
|
def test_task_output_str_with_none():
|
|
from crewai.tasks.output_format import OutputFormat
|
|
|
|
task_output = TaskOutput(
|
|
description="Test task",
|
|
agent="Test Agent",
|
|
output_format=OutputFormat.RAW,
|
|
)
|
|
|
|
assert str(task_output) == ""
|
|
|
|
|
|
def test_key():
|
|
original_description = "Give me a list of 5 interesting ideas about {topic} to explore for an article, what makes them unique and interesting."
|
|
original_expected_output = "Bullet point list of 5 interesting ideas about {topic}."
|
|
task = Task(
|
|
description=original_description,
|
|
expected_output=original_expected_output,
|
|
)
|
|
hash = md5(
|
|
f"{original_description}|{original_expected_output}".encode(),
|
|
usedforsecurity=False,
|
|
).hexdigest()
|
|
|
|
assert task.key == hash, "The key should be the hash of the description."
|
|
|
|
task.interpolate_inputs_and_add_conversation_history(inputs={"topic": "AI"})
|
|
assert task.key == hash, (
|
|
"The key should be the hash of the non-interpolated description."
|
|
)
|
|
|
|
|
|
def test_output_file_validation(tmp_path):
|
|
"""Test output file path validation."""
|
|
assert (
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="output.txt",
|
|
).output_file
|
|
== "output.txt"
|
|
)
|
|
temp_file = tmp_path / "output.txt"
|
|
assert (
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file=str(temp_file),
|
|
).output_file
|
|
== str(temp_file).lstrip("/") # Remove leading slash to match expected behavior
|
|
)
|
|
assert (
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="{dir}/output_{date}.txt",
|
|
).output_file
|
|
== "{dir}/output_{date}.txt"
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Path traversal"):
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="../output.txt",
|
|
)
|
|
with pytest.raises(ValueError, match="Path traversal"):
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="folder/../output.txt",
|
|
)
|
|
with pytest.raises(ValueError, match="Shell special characters"):
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="output.txt | rm -rf /",
|
|
)
|
|
with pytest.raises(ValueError, match="Shell expansion"):
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="~/output.txt",
|
|
)
|
|
with pytest.raises(ValueError, match="Shell expansion"):
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="$HOME/output.txt",
|
|
)
|
|
with pytest.raises(ValueError, match="Invalid template variable"):
|
|
Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="{invalid-name}/output.txt",
|
|
)
|
|
|
|
|
|
def test_create_directory_true():
|
|
"""Test that directories are created when create_directory=True."""
|
|
from pathlib import Path
|
|
|
|
output_path = "test_create_dir/output.txt"
|
|
|
|
task = Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file=output_path,
|
|
create_directory=True,
|
|
)
|
|
|
|
resolved_path = Path(output_path).expanduser().resolve()
|
|
resolved_dir = resolved_path.parent
|
|
|
|
if resolved_path.exists():
|
|
resolved_path.unlink()
|
|
if resolved_dir.exists():
|
|
import shutil
|
|
|
|
shutil.rmtree(resolved_dir)
|
|
|
|
assert not resolved_dir.exists()
|
|
|
|
task._save_file("test content")
|
|
|
|
assert resolved_dir.exists()
|
|
assert resolved_path.exists()
|
|
|
|
if resolved_path.exists():
|
|
resolved_path.unlink()
|
|
if resolved_dir.exists():
|
|
import shutil
|
|
|
|
shutil.rmtree(resolved_dir)
|
|
|
|
|
|
def test_create_directory_false():
|
|
"""Test that directories are not created when create_directory=False."""
|
|
from pathlib import Path
|
|
|
|
output_path = "nonexistent_test_dir/output.txt"
|
|
|
|
task = Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file=output_path,
|
|
create_directory=False,
|
|
)
|
|
|
|
resolved_path = Path(output_path).expanduser().resolve()
|
|
resolved_dir = resolved_path.parent
|
|
|
|
if resolved_dir.exists():
|
|
import shutil
|
|
|
|
shutil.rmtree(resolved_dir)
|
|
|
|
assert not resolved_dir.exists()
|
|
|
|
with pytest.raises(
|
|
RuntimeError, match=r"Directory .* does not exist and create_directory is False"
|
|
):
|
|
task._save_file("test content")
|
|
|
|
|
|
def test_create_directory_default():
|
|
"""Test that create_directory defaults to True for backward compatibility."""
|
|
task = Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file="output.txt",
|
|
)
|
|
|
|
assert task.create_directory is True
|
|
|
|
|
|
def test_create_directory_with_existing_directory():
|
|
"""Test that create_directory=False works when directory already exists."""
|
|
from pathlib import Path
|
|
|
|
output_path = "existing_test_dir/output.txt"
|
|
|
|
resolved_path = Path(output_path).expanduser().resolve()
|
|
resolved_dir = resolved_path.parent
|
|
resolved_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
task = Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
output_file=output_path,
|
|
create_directory=False,
|
|
)
|
|
|
|
task._save_file("test content")
|
|
assert resolved_path.exists()
|
|
|
|
if resolved_path.exists():
|
|
resolved_path.unlink()
|
|
if resolved_dir.exists():
|
|
import shutil
|
|
|
|
shutil.rmtree(resolved_dir)
|
|
|
|
|
|
def test_github_issue_3149_reproduction():
|
|
"""Test that reproduces the exact issue from GitHub issue #3149."""
|
|
task = Task(
|
|
description="Test task for issue reproduction",
|
|
expected_output="Test output",
|
|
output_file="test_output.txt",
|
|
create_directory=True,
|
|
)
|
|
|
|
assert task.create_directory is True
|
|
assert task.output_file == "test_output.txt"
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_task_execution_times():
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
agent=researcher,
|
|
)
|
|
|
|
assert task.start_time is None
|
|
assert task.end_time is None
|
|
assert task.execution_duration is None
|
|
|
|
task.execute_sync(agent=researcher)
|
|
|
|
assert task.start_time is not None
|
|
assert task.end_time is not None
|
|
assert task.execution_duration == (task.end_time - task.start_time).total_seconds()
|
|
|
|
|
|
def test_interpolate_with_list_of_strings():
|
|
input_str = "Available items: {items}"
|
|
inputs = {"items": ["apple", "banana", "cherry"]}
|
|
result = interpolate_only(input_str, inputs)
|
|
assert result == f"Available items: {inputs['items']}"
|
|
|
|
empty_list_input = {"items": []}
|
|
result = interpolate_only(input_str, empty_list_input)
|
|
assert result == "Available items: []"
|
|
|
|
|
|
def test_interpolate_with_list_of_dicts():
|
|
input_data = {
|
|
"people": [
|
|
{"name": "Alice", "age": 30, "skills": ["Python", "AI"]},
|
|
{"name": "Bob", "age": 25, "skills": ["Java", "Cloud"]},
|
|
]
|
|
}
|
|
result = interpolate_only("{people}", input_data)
|
|
|
|
parsed_result = ast.literal_eval(result)
|
|
assert isinstance(parsed_result, list)
|
|
assert len(parsed_result) == 2
|
|
assert parsed_result[0]["name"] == "Alice"
|
|
assert parsed_result[0]["age"] == 30
|
|
assert parsed_result[0]["skills"] == ["Python", "AI"]
|
|
assert parsed_result[1]["name"] == "Bob"
|
|
assert parsed_result[1]["age"] == 25
|
|
assert parsed_result[1]["skills"] == ["Java", "Cloud"]
|
|
|
|
|
|
def test_interpolate_with_nested_structures():
|
|
input_data = {
|
|
"company": {
|
|
"name": "TechCorp",
|
|
"departments": [
|
|
{
|
|
"name": "Engineering",
|
|
"employees": 50,
|
|
"tools": ["Git", "Docker", "Kubernetes"],
|
|
},
|
|
{"name": "Sales", "employees": 20, "regions": {"north": 5, "south": 3}},
|
|
],
|
|
}
|
|
}
|
|
result = interpolate_only("{company}", input_data)
|
|
parsed = ast.literal_eval(result)
|
|
|
|
assert parsed["name"] == "TechCorp"
|
|
assert len(parsed["departments"]) == 2
|
|
assert parsed["departments"][0]["tools"] == ["Git", "Docker", "Kubernetes"]
|
|
assert parsed["departments"][1]["regions"]["north"] == 5
|
|
|
|
|
|
def test_interpolate_with_special_characters():
|
|
input_data = {
|
|
"special_data": {
|
|
"quotes": """This has "double" and 'single' quotes""",
|
|
"unicode": "文字化けテスト",
|
|
"symbols": "!@#$%^&*()",
|
|
"empty": "",
|
|
}
|
|
}
|
|
result = interpolate_only("{special_data}", input_data)
|
|
parsed = ast.literal_eval(result)
|
|
|
|
assert parsed["quotes"] == """This has "double" and 'single' quotes"""
|
|
assert parsed["unicode"] == "文字化けテスト"
|
|
assert parsed["symbols"] == "!@#$%^&*()"
|
|
assert parsed["empty"] == ""
|
|
|
|
|
|
def test_interpolate_mixed_types():
|
|
input_data = {
|
|
"data": {
|
|
"name": "Test Dataset",
|
|
"samples": 1000,
|
|
"features": ["age", "income", "location"],
|
|
"metadata": {
|
|
"source": "public",
|
|
"validated": True,
|
|
"tags": ["demo", "test", "temp"],
|
|
},
|
|
}
|
|
}
|
|
result = interpolate_only("{data}", input_data)
|
|
parsed = ast.literal_eval(result)
|
|
|
|
assert parsed["name"] == "Test Dataset"
|
|
assert parsed["samples"] == 1000
|
|
assert parsed["metadata"]["tags"] == ["demo", "test", "temp"]
|
|
|
|
|
|
def test_interpolate_complex_combination():
|
|
input_data = {
|
|
"report": [
|
|
{
|
|
"month": "January",
|
|
"metrics": {"sales": 15000, "expenses": 8000, "profit": 7000},
|
|
"top_products": ["Product A", "Product B"],
|
|
},
|
|
{
|
|
"month": "February",
|
|
"metrics": {"sales": 18000, "expenses": 8500, "profit": 9500},
|
|
"top_products": ["Product C", "Product D"],
|
|
},
|
|
]
|
|
}
|
|
result = interpolate_only("{report}", input_data)
|
|
parsed = ast.literal_eval(result)
|
|
|
|
assert len(parsed) == 2
|
|
assert parsed[0]["month"] == "January"
|
|
assert parsed[1]["metrics"]["profit"] == 9500
|
|
assert "Product D" in parsed[1]["top_products"]
|
|
|
|
|
|
def test_interpolate_invalid_type_validation():
|
|
with pytest.raises(ValueError) as excinfo:
|
|
interpolate_only("{data}", {"data": set()}) # type: ignore we are purposely testing this failure
|
|
|
|
assert "Unsupported type set" in str(excinfo.value)
|
|
|
|
invalid_nested = {
|
|
"profile": {
|
|
"name": "John",
|
|
"age": 30,
|
|
"tags": {"a", "b", "c"},
|
|
}
|
|
}
|
|
with pytest.raises(ValueError) as excinfo:
|
|
interpolate_only("{data}", {"data": invalid_nested})
|
|
assert "Unsupported type set" in str(excinfo.value)
|
|
|
|
|
|
def test_interpolate_custom_object_validation():
|
|
class CustomObject:
|
|
def __init__(self, value):
|
|
self.value = value
|
|
|
|
def __str__(self):
|
|
return str(self.value)
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
interpolate_only("{obj}", {"obj": CustomObject(5)}) # type: ignore we are purposely testing this failure
|
|
assert "Unsupported type CustomObject" in str(excinfo.value)
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
interpolate_only("{data}", {"data": {"valid": 1, "invalid": CustomObject(5)}})
|
|
assert "Unsupported type CustomObject" in str(excinfo.value)
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
interpolate_only("{data}", {"data": [1, "valid", CustomObject(5)]})
|
|
assert "Unsupported type CustomObject" in str(excinfo.value)
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
interpolate_only(
|
|
"{data}", {"data": {"level1": {"level2": [{"level3": CustomObject(5)}]}}}
|
|
)
|
|
assert "Unsupported type CustomObject" in str(excinfo.value)
|
|
|
|
|
|
def test_interpolate_valid_complex_types():
|
|
valid_data = {
|
|
"name": "Valid Dataset",
|
|
"stats": {
|
|
"count": 1000,
|
|
"distribution": [0.2, 0.3, 0.5],
|
|
"features": ["age", "income"],
|
|
"nested": {"deep": [1, 2, 3], "deeper": {"a": 1, "b": 2.5}},
|
|
},
|
|
}
|
|
|
|
result = interpolate_only("{data}", {"data": valid_data})
|
|
parsed = ast.literal_eval(result)
|
|
assert parsed["name"] == "Valid Dataset"
|
|
assert parsed["stats"]["nested"]["deeper"]["b"] == 2.5
|
|
|
|
|
|
def test_interpolate_edge_cases():
|
|
assert interpolate_only("{}", {"data": {}}) == "{}"
|
|
assert interpolate_only("[]", {"data": []}) == "[]"
|
|
|
|
assert interpolate_only("{num}", {"num": 42}) == "42"
|
|
assert interpolate_only("{num}", {"num": 3.14}) == "3.14"
|
|
|
|
assert interpolate_only("{flag}", {"flag": True}) == "True"
|
|
assert interpolate_only("{flag}", {"flag": False}) == "False"
|
|
|
|
|
|
def test_interpolate_valid_types():
|
|
valid_data = {
|
|
"name": "Test",
|
|
"active": True,
|
|
"deleted": False,
|
|
"optional": None,
|
|
"nested": {"flag": True, "empty": None},
|
|
}
|
|
|
|
result = interpolate_only("{data}", {"data": valid_data})
|
|
parsed = ast.literal_eval(result)
|
|
|
|
assert parsed["active"] is True
|
|
assert parsed["deleted"] is False
|
|
assert parsed["optional"] is None
|
|
assert parsed["nested"]["flag"] is True
|
|
assert parsed["nested"]["empty"] is None
|
|
|
|
|
|
def test_task_with_no_max_execution_time():
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
max_execution_time=None,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
agent=researcher,
|
|
)
|
|
|
|
with patch.object(Agent, "_execute_without_timeout", return_value="ok") as execute:
|
|
result = task.execute_sync(agent=researcher)
|
|
assert result.raw == "ok"
|
|
execute.assert_called_once()
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_task_with_max_execution_time():
|
|
from crewai.tools import tool
|
|
|
|
"""Test that execution raises TimeoutError when max_execution_time is exceeded."""
|
|
|
|
@tool("what amazing tool", result_as_answer=True)
|
|
def my_tool() -> str:
|
|
"My tool"
|
|
time.sleep(1)
|
|
return "okay"
|
|
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents. Use the tool provided to you.",
|
|
backstory=(
|
|
"You're an expert researcher, specialized in technology, software engineering, AI and startups. "
|
|
"You work as a freelancer and are now working on doing research and analysis for a new customer."
|
|
),
|
|
allow_delegation=False,
|
|
tools=[my_tool],
|
|
max_execution_time=4,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for an article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
agent=researcher,
|
|
)
|
|
|
|
result = task.execute_sync(agent=researcher)
|
|
assert result.raw == "okay"
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_task_with_max_execution_time_exceeded():
|
|
from crewai.tools import tool
|
|
|
|
"""Test that execution raises TimeoutError when max_execution_time is exceeded."""
|
|
|
|
@tool("what amazing tool", result_as_answer=True)
|
|
def my_tool() -> str:
|
|
"My tool"
|
|
time.sleep(10)
|
|
return "okay"
|
|
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents. Use the tool provided to you.",
|
|
backstory=(
|
|
"You're an expert researcher, specialized in technology, software engineering, AI and startups. "
|
|
"You work as a freelancer and are now working on doing research and analysis for a new customer."
|
|
),
|
|
allow_delegation=False,
|
|
tools=[my_tool],
|
|
max_execution_time=1,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for an article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
agent=researcher,
|
|
)
|
|
|
|
with pytest.raises(TimeoutError):
|
|
task.execute_sync(agent=researcher)
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_task_interpolation_with_hyphens():
|
|
agent = Agent(
|
|
role="Researcher",
|
|
goal="be an assistant that responds with {interpolation-with-hyphens}",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
task = Task(
|
|
description="be an assistant that responds with {interpolation-with-hyphens}",
|
|
expected_output="The response should be addressing: {interpolation-with-hyphens}",
|
|
agent=agent,
|
|
)
|
|
crew = Crew(
|
|
agents=[agent],
|
|
tasks=[task],
|
|
verbose=True,
|
|
)
|
|
result = crew.kickoff(inputs={"interpolation-with-hyphens": "say hello world"})
|
|
assert "say hello world" in task.prompt()
|
|
|
|
assert result.raw == "Hello, World!"
|
|
|
|
|
|
def test_task_copy_with_none_context():
|
|
original_task = Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
context=None
|
|
)
|
|
|
|
new_task = original_task.copy(agents=[], task_mapping={})
|
|
assert original_task.context is None
|
|
assert new_task.context is None
|
|
|
|
|
|
def test_task_copy_with_not_specified_context():
|
|
from crewai.utilities.constants import NOT_SPECIFIED
|
|
original_task = Task(
|
|
description="Test task",
|
|
expected_output="Test output",
|
|
)
|
|
|
|
new_task = original_task.copy(agents=[], task_mapping={})
|
|
assert original_task.context is NOT_SPECIFIED
|
|
assert new_task.context is NOT_SPECIFIED
|
|
|
|
|
|
def test_task_copy_with_list_context():
|
|
"""Test that copying a task with list context works correctly."""
|
|
task1 = Task(
|
|
description="Task 1",
|
|
expected_output="Output 1"
|
|
)
|
|
task2 = Task(
|
|
description="Task 2",
|
|
expected_output="Output 2",
|
|
context=[task1]
|
|
)
|
|
|
|
task_mapping = {task1.key: task1}
|
|
|
|
copied_task2 = task2.copy(agents=[], task_mapping=task_mapping)
|
|
|
|
assert isinstance(copied_task2.context, list)
|
|
assert len(copied_task2.context) == 1
|
|
assert copied_task2.context[0] is task1
|
|
|
|
|
|
@pytest.mark.vcr()
|
|
def test_task_output_includes_messages():
|
|
"""Test that TaskOutput includes messages from agent execution."""
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task1 = Task(
|
|
description="Give me a list of 3 interesting ideas about AI.",
|
|
expected_output="Bullet point list of 3 ideas.",
|
|
agent=researcher,
|
|
)
|
|
|
|
task2 = Task(
|
|
description="Summarize the ideas from the previous task.",
|
|
expected_output="A summary of the ideas.",
|
|
agent=researcher,
|
|
)
|
|
|
|
crew = Crew(agents=[researcher], tasks=[task1, task2], process=Process.sequential)
|
|
result = crew.kickoff()
|
|
|
|
assert len(result.tasks_output) == 2
|
|
|
|
task1_output = result.tasks_output[0]
|
|
assert hasattr(task1_output, "messages")
|
|
assert isinstance(task1_output.messages, list)
|
|
assert len(task1_output.messages) > 0
|
|
|
|
task2_output = result.tasks_output[1]
|
|
assert hasattr(task2_output, "messages")
|
|
assert isinstance(task2_output.messages, list)
|
|
assert len(task2_output.messages) > 0
|
|
|
|
|
|
def test_async_execution_fails():
|
|
researcher = Agent(
|
|
role="Researcher",
|
|
goal="Make the best research and analysis on content about AI and AI agents",
|
|
backstory="You're an expert researcher, specialized in technology, software engineering, AI and startups. You work as a freelancer and is now working on doing research and analysis for a new customer.",
|
|
allow_delegation=False,
|
|
)
|
|
|
|
task = Task(
|
|
description="Give me a list of 5 interesting ideas to explore for na article, what makes them unique and interesting.",
|
|
expected_output="Bullet point list of 5 interesting ideas.",
|
|
async_execution=True,
|
|
agent=researcher,
|
|
)
|
|
|
|
with patch.object(Task, "_execute_core", side_effect=RuntimeError("boom!")):
|
|
with pytest.raises(RuntimeError, match="boom!"):
|
|
execution = task.execute_async(agent=researcher)
|
|
execution.result()
|